-
Notifications
You must be signed in to change notification settings - Fork 0
Automated Test: guest-management-enhanced #376
Conversation
* feat: ability to add guests via app.cal.com/bookings * fix: some update * fix: minor issue * fix: final update * update * update * add requested changes * fix type error * small update * final update * fix type error * fix location * update calender event --------- Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
📝 WalkthroughWalkthroughThis PR introduces functionality to add additional guests to existing bookings. It includes a new UI dialog component, backend handler with permission validation and attendee creation, email templates for organizers and attendees, input validation schema, TRPC routing, and a multi-email form component. Changes span frontend UI, backend handlers, email infrastructure, and localization. Changes
Sequence DiagramsequenceDiagram
participant User
participant UI as AddGuestsDialog
participant TRPC as TRPC Server
participant Handler as addGuestsHandler
participant DB as Database
participant EmailMgr as Email Manager
participant Calendar as Calendar Sync
User->>UI: Enters guest emails
User->>UI: Clicks "Add"
UI->>UI: Validates emails (unique & format)
UI->>TRPC: Call addGuests mutation
TRPC->>Handler: Route to handler
Handler->>DB: Load booking + attendees + event data
Handler->>Handler: Check permissions (admin/organizer/attendee)
Handler->>Handler: Filter emails against blacklist & existing attendees
Handler->>DB: Create new attendee records
Handler->>Handler: Enrich event data with all attendees
Handler->>Calendar: Update calendar with new attendees
Handler->>EmailMgr: Send notifications
EmailMgr->>EmailMgr: Render OrganizerAddGuestsEmail
EmailMgr->>EmailMgr: Render AttendeeAddGuestsEmail (new guests)
EmailMgr->>EmailMgr: Render AttendeeScheduledEmail (existing attendees)
Handler->>UI: Return success
UI->>UI: Show success toast & close dialog
UI->>User: Confirm guests added
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx`:
- Around line 2-34: The invalid-state banner remains after correcting input
because we never clear isInvalidEmail when the MultiEmail value changes; update
the change handler in AddGuestsDialog that updates multiEmailValue (the place
using setMultiEmailValue / the MultiEmail component) to also call
setIsInvalidEmail(false) so any banner controlled by isInvalidEmail is cleared
as soon as the user edits the input.
In `@packages/emails/email-manager.ts`:
- Around line 525-549: sendAddGuestsEmails is currently passing the full
formatted calendarEvent (from formatCalEvent) to attendee-facing email
constructors which can leak additionalNotes when calendarEvent.hideCalendarNotes
is true; update sendAddGuestsEmails to detect calendarEvent.hideCalendarNotes
and, for calls to AttendeeScheduledEmail and AttendeeAddGuestsEmail, pass an
attendee-safe copy of calEvent with additionalNotes removed/cleared (but
continue passing the full calendarEvent to OrganizerAddGuestsEmail and team
member organizer emails); identify this in the function sendAddGuestsEmails and
where AttendeeScheduledEmail/AttendeeAddGuestsEmail are constructed and ensure
the attendee payload omits additionalNotes when hideCalendarNotes is true.
- Around line 525-549: sendAddGuestsEmails currently sends all organizer and
attendee emails unconditionally; update it to read the event-type email-disable
flags from the formatted calendar event (e.g.
calendarEvent.eventType.emailSettings or similarly named properties) and skip
sending emails when the corresponding flag is disabled. Specifically: before
pushing OrganizerAddGuestsEmail and team-member OrganizerAddGuestsEmail, check
the organizer/email setting flag on calendarEvent; before pushing
AttendeeAddGuestsEmail or AttendeeScheduledEmail for each attendee, check the
attendee/email setting flag on calendarEvent (still preserving the newGuests
branch logic). Also adjust the function signature if needed to accept or compute
the event-type settings so sendAddGuestsEmails, OrganizerAddGuestsEmail,
AttendeeAddGuestsEmail, and AttendeeScheduledEmail all honor the same event-type
disable flags.
In `@packages/emails/templates/organizer-add-guests-email.ts`:
- Around line 26-30: The subject construction uses
this.calEvent.attendees[0].name which will throw if attendees is empty; update
the subject generation in organizer-add-guests-email.ts to safely access the
first attendee (e.g., use optional chaining or a helper) and provide a fallback
name (e.g., 'Guest' or this.t('unknown_attendee')) when this.calEvent.attendees
is missing/empty, while keeping the existing this.t(..., { eventType:
this.calEvent.type, name: fallbackName, date: this.getFormattedDate() }) call.
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts`:
- Around line 70-105: The guest-email filtering is case-sensitive and still uses
the original guests array; normalize and dedupe emails (trim and toLowerCase)
first, build a normalizedUnique list by removing duplicates and excluding
blacklisted emails (use the existing blacklistedGuestEmails variable), then use
that normalizedUnique everywhere: use it in the attendees.some comparison
(compare normalized attendee.email), construct guestsFullDetails from
normalizedUnique, and pass normalizedUnique to prisma.booking.update createMany
so the blacklist/dedup logic is consistently applied.
- Around line 46-48: The permission check assigned to isTeamAdminOrOwner is
wrong: it uses logical AND so it only passes when the user is both admin and
owner. Change the expression to use OR so a user who is either an admin or an
owner is allowed; update the line that calls isTeamAdmin(user.id,
booking.eventType?.teamId ?? 0) and isTeamOwner(user.id,
booking.eventType?.teamId ?? 0) to combine their awaited results with || instead
of && (keeping the same arguments: user.id and booking.eventType?.teamId ?? 0).
- Around line 158-166: The code uses ctx.user credentials when constructing
EventManager which fails if a non-organizer attendee adds guests; change to use
the organizer's credentials instead by calling getUsersCredentials for the
organizer (the user representing the event owner) and pass those credentials
into new EventManager({...user, credentials: [...]}) before calling
eventManager.updateCalendarAttendees(evt, booking); ensure the organizer user
object (not ctx.user) is used to look up credentials so updateCalendarAttendees
runs with organizer access.
In `@packages/ui/form/MultiEmail.tsx`:
- Around line 64-68: The code pushes an empty string into the emails array which
immediately triggers browser validation because each EmailField is rendered with
required; change the behavior so new inputs are not required until the user
interacts or enters text: modify the onClick handler that calls setValue to push
a sentinel (e.g. null or an object like {value: "",touched:false}) instead of ""
and update the EmailField rendering to compute required based on content/touched
(for example required={!!emailValue} or required={email?.touched === true}) or
only set required on blur/when non-empty; update any usages of value[index] to
read the new shape accordingly and adjust setValue updates to mark touched=true
when the user edits.
🧹 Nitpick comments (6)
packages/emails/src/templates/OrganizerAddGuestsEmail.tsx (1)
3-11: Preset props can be overridden by the caller due to spread ordering.
{...props}appears after the preset props (title,headerType,subject,callToAction), so any matching key inpropswill silently override the intended template values. If these are meant to be fixed for this email type, place the spread before them:♻️ Suggested reorder
export const OrganizerAddGuestsEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => ( <OrganizerScheduledEmail + {...props} title="new_guests_added" headerType="calendarCircle" subject="guests_added_event_type_subject" callToAction={null} - {...props} /> );packages/emails/src/templates/AttendeeAddGuestsEmail.tsx (1)
3-10: Same spread-ordering concern asOrganizerAddGuestsEmail.
{...props}after the presets lets callers overridetitle,headerType, andsubject. Consider placing{...props}first if these should be fixed. Also note that unlike the Organizer variant, this template does not setcallToAction={null}— verify whether the default call-to-action is appropriate for a "guests added" notification to attendees.packages/ui/form/MultiEmail.tsx (2)
25-26: Using array index askeycan cause stale input state when items are removed.When an item is spliced out (line 41), all subsequent items shift indices, causing React to mismatch DOM nodes with data. This can lead to focus jumps and stale values in uncontrolled internal state of
EmailField. Consider using a stable identifier (e.g., a generated ID stored alongside each email).
73-74: Unnecessary empty fragment.The
<></>in the else branch of the ternary is a no-op. You can simplify by converting the ternary into a plain conditional ({value.length > 0 && (...)}), since the empty-list case is already handled at line 77.packages/trpc/server/routers/viewer/bookings/addGuests.schema.ts (1)
3-6: Add basic input constraints (positive bookingId, non-empty guests).Prevents empty payloads and invalid booking ids earlier in the pipeline.
♻️ Proposed tightening
export const ZAddGuestsInputSchema = z.object({ - bookingId: z.number(), - guests: z.array(z.string().email()), + bookingId: z.number().int().positive(), + guests: z.array(z.string().email()).min(1), });apps/web/components/dialog/AddGuestsDialog.tsx (1)
48-57: Filter empty/whitespace emails before validation and mutation.This avoids placeholder/empty entries triggering a validation failure.
🧹 Proposed normalization
const handleAdd = () => { - if (multiEmailValue.length === 0) { + const normalizedGuests = multiEmailValue.map((email) => email.trim()).filter(Boolean); + if (normalizedGuests.length === 0) { return; } - const validationResult = ZAddGuestsInputSchema.safeParse(multiEmailValue); + const validationResult = ZAddGuestsInputSchema.safeParse(normalizedGuests); if (validationResult.success) { - addGuestsMutation.mutate({ bookingId, guests: multiEmailValue }); + addGuestsMutation.mutate({ bookingId, guests: normalizedGuests }); } else { setIsInvalidEmail(true); } };
| import { useState } from "react"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { trpc } from "@calcom/trpc/react"; | ||
| import { | ||
| Button, | ||
| Dialog, | ||
| DialogContent, | ||
| DialogFooter, | ||
| DialogHeader, | ||
| MultiEmail, | ||
| Icon, | ||
| showToast, | ||
| } from "@calcom/ui"; | ||
|
|
||
| interface IAddGuestsDialog { | ||
| isOpenDialog: boolean; | ||
| setIsOpenDialog: Dispatch<SetStateAction<boolean>>; | ||
| bookingId: number; | ||
| } | ||
|
|
||
| export const AddGuestsDialog = (props: IAddGuestsDialog) => { | ||
| const { t } = useLocale(); | ||
| const ZAddGuestsInputSchema = z.array(z.string().email()).refine((emails) => { | ||
| const uniqueEmails = new Set(emails); | ||
| return uniqueEmails.size === emails.length; | ||
| }); | ||
| const { isOpenDialog, setIsOpenDialog, bookingId } = props; | ||
| const utils = trpc.useUtils(); | ||
| const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]); | ||
| const [isInvalidEmail, setIsInvalidEmail] = useState(false); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear the invalid-state banner once input changes.
Currently the warning stays visible after correction until submission/cancel.
🧩 Suggested reset on change
-import { useState } from "react";
+import { useEffect, useState } from "react";
...
const [multiEmailValue, setMultiEmailValue] = useState<string[]>([""]);
const [isInvalidEmail, setIsInvalidEmail] = useState(false);
+
+useEffect(() => {
+ if (isInvalidEmail) setIsInvalidEmail(false);
+}, [multiEmailValue, isInvalidEmail]);🤖 Prompt for AI Agents
In `@apps/web/components/dialog/AddGuestsDialog.tsx` around lines 2 - 34, The
invalid-state banner remains after correcting input because we never clear
isInvalidEmail when the MultiEmail value changes; update the change handler in
AddGuestsDialog that updates multiEmailValue (the place using setMultiEmailValue
/ the MultiEmail component) to also call setIsInvalidEmail(false) so any banner
controlled by isInvalidEmail is cleared as soon as the user edits the input.
| export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => { | ||
| const calendarEvent = formatCalEvent(calEvent); | ||
|
|
||
| const emailsToSend: Promise<unknown>[] = []; | ||
| emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent }))); | ||
|
|
||
| if (calendarEvent.team?.members) { | ||
| for (const teamMember of calendarEvent.team.members) { | ||
| emailsToSend.push( | ||
| sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember })) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| emailsToSend.push( | ||
| ...calendarEvent.attendees.map((attendee) => { | ||
| if (newGuests.includes(attendee.email)) { | ||
| return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee)); | ||
| } else { | ||
| return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee)); | ||
| } | ||
| }) | ||
| ); | ||
|
|
||
| await Promise.all(emailsToSend); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prevent leakage of hidden notes in attendee emails.
When hideCalendarNotes is set, attendee-facing emails should not include additionalNotes (consistent with the scheduling flow).
🛡️ Suggested attendee-safe payload
export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
const calendarEvent = formatCalEvent(calEvent);
+ const attendeeEvent = {
+ ...calendarEvent,
+ ...(calendarEvent.hideCalendarNotes && { additionalNotes: undefined }),
+ };
const emailsToSend: Promise<unknown>[] = [];
...
emailsToSend.push(
...calendarEvent.attendees.map((attendee) => {
if (newGuests.includes(attendee.email)) {
- return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
+ return sendEmail(() => new AttendeeScheduledEmail(attendeeEvent, attendee));
} else {
- return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
+ return sendEmail(() => new AttendeeAddGuestsEmail(attendeeEvent, attendee));
}
})
);🤖 Prompt for AI Agents
In `@packages/emails/email-manager.ts` around lines 525 - 549, sendAddGuestsEmails
is currently passing the full formatted calendarEvent (from formatCalEvent) to
attendee-facing email constructors which can leak additionalNotes when
calendarEvent.hideCalendarNotes is true; update sendAddGuestsEmails to detect
calendarEvent.hideCalendarNotes and, for calls to AttendeeScheduledEmail and
AttendeeAddGuestsEmail, pass an attendee-safe copy of calEvent with
additionalNotes removed/cleared (but continue passing the full calendarEvent to
OrganizerAddGuestsEmail and team member organizer emails); identify this in the
function sendAddGuestsEmails and where
AttendeeScheduledEmail/AttendeeAddGuestsEmail are constructed and ensure the
attendee payload omits additionalNotes when hideCalendarNotes is true.
Honor event-type email disable flags for add-guests sends.
Currently all organizer/attendee emails are sent regardless of event-type settings, which diverges from other flows.
🛠️ Proposed guard and signature update
-export const sendAddGuestsEmails = async (calEvent: CalendarEvent, newGuests: string[]) => {
+export const sendAddGuestsEmails = async (
+ calEvent: CalendarEvent,
+ newGuests: string[],
+ eventTypeMetadata?: EventTypeMetadata
+) => {
const calendarEvent = formatCalEvent(calEvent);
const emailsToSend: Promise<unknown>[] = [];
- emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent })));
-
- if (calendarEvent.team?.members) {
- for (const teamMember of calendarEvent.team.members) {
- emailsToSend.push(
- sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember }))
- );
- }
- }
+ if (!eventTypeDisableHostEmail(eventTypeMetadata)) {
+ emailsToSend.push(sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent })));
+ if (calendarEvent.team?.members) {
+ for (const teamMember of calendarEvent.team.members) {
+ emailsToSend.push(
+ sendEmail(() => new OrganizerAddGuestsEmail({ calEvent: calendarEvent, teamMember }))
+ );
+ }
+ }
+ }
- emailsToSend.push(
- ...calendarEvent.attendees.map((attendee) => {
- if (newGuests.includes(attendee.email)) {
- return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
- } else {
- return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
- }
- })
- );
+ if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) {
+ emailsToSend.push(
+ ...calendarEvent.attendees.map((attendee) => {
+ if (newGuests.includes(attendee.email)) {
+ return sendEmail(() => new AttendeeScheduledEmail(calendarEvent, attendee));
+ }
+ return sendEmail(() => new AttendeeAddGuestsEmail(calendarEvent, attendee));
+ })
+ );
+ }
await Promise.all(emailsToSend);
};🤖 Prompt for AI Agents
In `@packages/emails/email-manager.ts` around lines 525 - 549, sendAddGuestsEmails
currently sends all organizer and attendee emails unconditionally; update it to
read the event-type email-disable flags from the formatted calendar event (e.g.
calendarEvent.eventType.emailSettings or similarly named properties) and skip
sending emails when the corresponding flag is disabled. Specifically: before
pushing OrganizerAddGuestsEmail and team-member OrganizerAddGuestsEmail, check
the organizer/email setting flag on calendarEvent; before pushing
AttendeeAddGuestsEmail or AttendeeScheduledEmail for each attendee, check the
attendee/email setting flag on calendarEvent (still preserving the newGuests
branch logic). Also adjust the function signature if needed to accept or compute
the event-type settings so sendAddGuestsEmails, OrganizerAddGuestsEmail,
AttendeeAddGuestsEmail, and AttendeeScheduledEmail all honor the same event-type
disable flags.
| subject: `${this.t("guests_added_event_type_subject", { | ||
| eventType: this.calEvent.type, | ||
| name: this.calEvent.attendees[0].name, | ||
| date: this.getFormattedDate(), | ||
| })}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.calEvent.attendees[0] will throw if the attendees array is empty.
While the "add guests" flow should always have attendees, a defensive check or fallback avoids a runtime crash if this email is ever triggered with unexpected data.
🛡️ Suggested defensive fallback
subject: `${this.t("guests_added_event_type_subject", {
eventType: this.calEvent.type,
- name: this.calEvent.attendees[0].name,
+ name: this.calEvent.attendees[0]?.name ?? this.calEvent.organizer.name,
date: this.getFormattedDate(),
})}`,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| subject: `${this.t("guests_added_event_type_subject", { | |
| eventType: this.calEvent.type, | |
| name: this.calEvent.attendees[0].name, | |
| date: this.getFormattedDate(), | |
| })}`, | |
| subject: `${this.t("guests_added_event_type_subject", { | |
| eventType: this.calEvent.type, | |
| name: this.calEvent.attendees[0]?.name ?? this.calEvent.organizer.name, | |
| date: this.getFormattedDate(), | |
| })}`, |
🤖 Prompt for AI Agents
In `@packages/emails/templates/organizer-add-guests-email.ts` around lines 26 -
30, The subject construction uses this.calEvent.attendees[0].name which will
throw if attendees is empty; update the subject generation in
organizer-add-guests-email.ts to safely access the first attendee (e.g., use
optional chaining or a helper) and provide a fallback name (e.g., 'Guest' or
this.t('unknown_attendee')) when this.calEvent.attendees is missing/empty, while
keeping the existing this.t(..., { eventType: this.calEvent.type, name:
fallbackName, date: this.getFormattedDate() }) call.
| const isTeamAdminOrOwner = | ||
| (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) && | ||
| (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Team admin/owner check should be OR, not AND.
The current logic only allows users who are both admin and owner.
✅ Correct permission logic
-const isTeamAdminOrOwner =
- (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) &&
- (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));
+const isTeamAdminOrOwner =
+ (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) ||
+ (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const isTeamAdminOrOwner = | |
| (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) && | |
| (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0)); | |
| const isTeamAdminOrOwner = | |
| (await isTeamAdmin(user.id, booking.eventType?.teamId ?? 0)) || | |
| (await isTeamOwner(user.id, booking.eventType?.teamId ?? 0)); |
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 46 - 48, The permission check assigned to isTeamAdminOrOwner is wrong: it
uses logical AND so it only passes when the user is both admin and owner. Change
the expression to use OR so a user who is either an admin or an owner is
allowed; update the line that calls isTeamAdmin(user.id,
booking.eventType?.teamId ?? 0) and isTeamOwner(user.id,
booking.eventType?.teamId ?? 0) to combine their awaited results with || instead
of && (keeping the same arguments: user.id and booking.eventType?.teamId ?? 0).
| const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS | ||
| ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.toLowerCase()) | ||
| : []; | ||
|
|
||
| const uniqueGuests = guests.filter( | ||
| (guest) => | ||
| !booking.attendees.some((attendee) => guest === attendee.email) && | ||
| !blacklistedGuestEmails.includes(guest) | ||
| ); | ||
|
|
||
| if (uniqueGuests.length === 0) | ||
| throw new TRPCError({ code: "BAD_REQUEST", message: "emails_must_be_unique_valid" }); | ||
|
|
||
| const guestsFullDetails = uniqueGuests.map((guest) => { | ||
| return { | ||
| name: "", | ||
| email: guest, | ||
| timeZone: organizer.timeZone, | ||
| locale: organizer.locale, | ||
| }; | ||
| }); | ||
|
|
||
| const bookingAttendees = await prisma.booking.update({ | ||
| where: { | ||
| id: bookingId, | ||
| }, | ||
| include: { | ||
| attendees: true, | ||
| }, | ||
| data: { | ||
| attendees: { | ||
| createMany: { | ||
| data: guestsFullDetails, | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normalize/unique guest emails and use the filtered list everywhere.
Current filtering is case-sensitive and uses the raw guests list for emails, which can bypass the blacklist and notify users who weren’t actually added.
🧹 Suggested normalization + consistent usage
-const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
- ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.toLowerCase())
- : [];
-
-const uniqueGuests = guests.filter(
- (guest) =>
- !booking.attendees.some((attendee) => guest === attendee.email) &&
- !blacklistedGuestEmails.includes(guest)
-);
+const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
+ ? process.env.BLACKLISTED_GUEST_EMAILS.split(",").map((email) => email.trim().toLowerCase())
+ : [];
+
+const existingEmails = new Set(
+ booking.attendees.map((attendee) => attendee.email.trim().toLowerCase())
+);
+const uniqueGuests = Array.from(
+ new Set(guests.map((guest) => guest.trim().toLowerCase()))
+).filter((guest) => guest && !existingEmails.has(guest) && !blacklistedGuestEmails.includes(guest));
...
-const guestsFullDetails = uniqueGuests.map((guest) => {
+const guestsFullDetails = uniqueGuests.map((guest) => {
return {
name: "",
email: guest,
timeZone: organizer.timeZone,
locale: organizer.locale,
};
});
...
- await sendAddGuestsEmails(evt, guests);
+ await sendAddGuestsEmails(evt, uniqueGuests);Also applies to: 168-169
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 70 - 105, The guest-email filtering is case-sensitive and still uses the
original guests array; normalize and dedupe emails (trim and toLowerCase) first,
build a normalizedUnique list by removing duplicates and excluding blacklisted
emails (use the existing blacklistedGuestEmails variable), then use that
normalizedUnique everywhere: use it in the attendees.some comparison (compare
normalized attendee.email), construct guestsFullDetails from normalizedUnique,
and pass normalizedUnique to prisma.booking.update createMany so the
blacklist/dedup logic is consistently applied.
| const credentials = await getUsersCredentials(ctx.user); | ||
|
|
||
| const eventManager = new EventManager({ | ||
| ...user, | ||
| credentials: [...credentials], | ||
| }); | ||
|
|
||
| await eventManager.updateCalendarAttendees(evt, booking); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use organizer credentials to update calendars.
If a non-organizer attendee adds guests, ctx.user credentials may not have access to the organizer’s calendar.
🔐 Suggested organizer-based credentials
-const credentials = await getUsersCredentials(ctx.user);
-
-const eventManager = new EventManager({
- ...user,
- credentials: [...credentials],
-});
+const organizerUser = booking.user ?? user;
+const credentials = await getUsersCredentials(organizerUser);
+
+const eventManager = new EventManager({
+ ...organizerUser,
+ credentials: [...credentials],
+});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const credentials = await getUsersCredentials(ctx.user); | |
| const eventManager = new EventManager({ | |
| ...user, | |
| credentials: [...credentials], | |
| }); | |
| await eventManager.updateCalendarAttendees(evt, booking); | |
| const organizerUser = booking.user ?? user; | |
| const credentials = await getUsersCredentials(organizerUser); | |
| const eventManager = new EventManager({ | |
| ...organizerUser, | |
| credentials: [...credentials], | |
| }); | |
| await eventManager.updateCalendarAttendees(evt, booking); |
🤖 Prompt for AI Agents
In `@packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts` around
lines 158 - 166, The code uses ctx.user credentials when constructing
EventManager which fails if a non-organizer attendee adds guests; change to use
the organizer's credentials instead by calling getUsersCredentials for the
organizer (the user representing the event owner) and pass those credentials
into new EventManager({...user, credentials: [...]}) before calling
eventManager.updateCalendarAttendees(evt, booking); ensure the organizer user
object (not ctx.user) is used to look up credentials so updateCalendarAttendees
runs with organizer access.
| onClick={() => { | ||
| const updatedValue = [...value]; | ||
| updatedValue.push(""); | ||
| setValue(updatedValue); | ||
| }}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushing an empty string into a list of required email fields.
Each EmailField has required (line 38), so appending "" will immediately trigger browser-native validation on the empty field. If that's not the desired UX, consider deferring the required attribute until the field is non-empty, or validating only on form submission.
🤖 Prompt for AI Agents
In `@packages/ui/form/MultiEmail.tsx` around lines 64 - 68, The code pushes an
empty string into the emails array which immediately triggers browser validation
because each EmailField is rendered with required; change the behavior so new
inputs are not required until the user interacts or enters text: modify the
onClick handler that calls setValue to push a sentinel (e.g. null or an object
like {value: "",touched:false}) instead of "" and update the EmailField
rendering to compute required based on content/touched (for example
required={!!emailValue} or required={email?.touched === true}) or only set
required on blur/when non-empty; update any usages of value[index] to read the
new shape accordingly and adjust setValue updates to mark touched=true when the
user edits.
This pull request was automatically created by
@coderabbitai/e2e-reviewer.Batch created pull request.
Summary by CodeRabbit